package com.lemon.cloud.security.component;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.lemon.cloud.security.annotation.Inner;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 白名单URL = 配置文件ignore + @inner(false)
 *
 * 版本1问题：会暴露所有restful 风格的接，例如暴露：getUsername 的所有GET，POST等url，安全性低
 * 优化方案：filterPath() 标记 GET、POST
 *
 * @author HuangDS
 * @date 2020-03-11
 * <p>
 * 资源服务器对外直接暴露URL
 */
@Slf4j
@ConfigurationProperties(prefix = "security.oauth2.ignore")
public class PermitAllUrlProperties implements InitializingBean {

    private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");

    private static final String[] DEFAULT_IGNORE_URLS = new String[] { "/actuator/**", "/error", "/v3/api-docs" };

    @Getter
    @Setter
    private List<String> urls = new ArrayList<>();

    @Override
    public void afterPropertiesSet() {
        urls.addAll(Arrays.asList(DEFAULT_IGNORE_URLS));
        RequestMappingHandlerMapping mapping = SpringUtil.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();

        for (RequestMappingInfo info : map.keySet()) {
            HandlerMethod handlerMethod = map.get(info);
            // 1. 首先获取类上边 @Inner 注解
            Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);

            // 2. 当类上不包含 @Inner 注解则获取该方法的注解
            if (controller == null) {
                Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
                Optional.ofNullable(method).ifPresent(inner -> {
                    if (info.getPatternsCondition()!=null){
                        info.getPatternsCondition().getPatterns()
                                .forEach(url -> this.filterPath(url, info, map));
                    }else if (info.getPathPatternsCondition()!=null){
                        info.getPathPatternsCondition().getPatterns()
                                .forEach(url -> this.filterPath(url.getPatternString(), info, map));
                    }

                });
                continue;
            }

            // 3. 当类上包含 @Inner 注解 判断handlerMethod 是否包含在 inner 类中
            Class<?> beanType = handlerMethod.getBeanType();
            Method[] methods = beanType.getDeclaredMethods();
            Method method = handlerMethod.getMethod();
            if (ArrayUtil.contains(methods, method)) {
                if (info.getPatternsCondition()!=null){
                    info.getPatternsCondition().getPatterns()
                            .forEach(url -> this.filterPath(url, info, map));
                }else if (info.getPathPatternsCondition()!=null){
                    info.getPathPatternsCondition().getPatterns()
                            .forEach(url -> this.filterPath(url.getPatternString(), info, map));
                }
            }
        }
    }

    /**
     * 过滤 Inner 设置
     * <p>
     * 0. 暴露安全检查 1. 路径转换： 如果为restful(/xx/{xx}) --> /xx/* ant 表达式 2.
     * 构建表达式：允许暴露的接口|允许暴露的方法类型,允许暴露的方法类型 URL||GET,POST,DELETE,PUT
     * </p>
     * @param url mapping路径
     * @param info 请求犯法
     * @param map 路由映射信息
     */
    private void filterPath(String url, RequestMappingInfo info, Map<RequestMappingInfo, HandlerMethod> map) {
        List<String> methodList = info.getMethodsCondition().getMethods().stream().map(RequestMethod::name)
                .collect(Collectors.toList());
        String resultUrl = ReUtil.replaceAll(url, PATTERN, "*");
        if (CollUtil.isEmpty(methodList)) {
            urls.add(resultUrl);
        }
        else {
            urls.add(String.format("%s||%s", resultUrl, CollUtil.join(methodList, StrUtil.COMMA)));
        }
    }

    /**
     * 获取对外暴露的URL，注册到 spring security
     * @param registry spring security context
     */
    public void registry(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
        for (String url : getUrls()) {
            List<String> strings = StrUtil.split(url, "||");

            // 仅配置对外暴露的URL ，则注册到 spring security的为全部方法
            if (strings.size() == 1) {
                registry.requestMatchers(strings.get(0)).permitAll();
                continue;
            }

            // 当配置对外的URL|GET,POST 这种形式，则获取方法列表 并注册到 spring security
            if (strings.size() == 2) {
                for (String method : StrUtil.split(strings.get(1), StrUtil.COMMA)) {
                    registry.requestMatchers(HttpMethod.valueOf(method), strings.get(0)).permitAll();
                }
                continue;
            }

            log.warn("{} 配置无效，无法配置对外暴露", url);
        }
    }
}
